package comtacti.native.internal.enums;
import macrotools.TypeImports;
import macrotools.TypeTools;
import macrotools.MacroTools;
import macrotools.Imports;
using Lambda;

typedef EnumData = 
{
	resolverPath:Array<String>,
	valueType:ComplexType
}

class ValuedEnumBuild
{
	private static var valuedEnumResolvers:Hash<EnumData> = new Hash();
	
	public static function isValuedEnum(en:Ref<EnumType>):Bool
	{
		if (valuedEnumResolvers.exists(en.toString()))
			return true;
		
		return en.get().meta.get().exists(function(e) return e.name == ":helper");
	}
	
	public static function getEnumExpr(e:EnumType, pos:Position):Expr
	{
		var path = e.pack.copy();
		path.push(e.name);
		
		var info = getEnumData(path.join("."));
		if (info == null)
		{
			var helper = e.meta.get().filter(function(m) return m.name == ":helper").first();
			if (helper != null)
			{
				return {expr:EType(helper.params[0], e.name + "Values"), pos:pos};
			}
		}
			throw new Error("Enum '" + path.join(".") + "' is not a valid Valued Enum type. Maybe you forgot to add it as a @:subject of a Valued Enum class, or maybe you haven't declared the Valued Enum class it in the same file as the enum?", pos);
		
		return MacroTools.path(info.resolverPath, pos);
	}
	
	public static function getEnumData(path:String):Null<EnumData>
	{
		return valuedEnumResolvers.get(path);
	}
	
	public static function build():Array<Field>
	{
		var cls = Context.getLocalClass().get();
		var pos = cls.pos;
		
		var meta = cls.meta.get();
		
		var subjectMetas = meta.filter(function(m)return m.name == ":subject");
		if (subjectMetas.length == 0) throw new Error("Valued Enum must have a @:subject() meta associated with it, which points to a valid enum", pos);
		
		var fields = [];
		
		for (subjectMeta in subjectMetas)
		{
			var subjectPath = MacroTools.getString(subjectMeta.params[0], true, true);

			var subjectPathArr = subjectPath.split(".");
			var subjectPathName = subjectPathArr.pop();
			var clsName = subjectPathName;

			var enumComplexType = TPath({pack:subjectPathArr, name:subjectPathName, params:[], sub:null});
			var enumType = TypeTools.getType(enumComplexType, subjectMeta.params[0].pos);
			var exprArray = [];
			var valuesType = null;
			var valuesTypeStr = null;

			switch(enumType)
			{
				case TEnum(e, _):
					var e = e.get();
					var values = e.meta.get().filter(function(m) return m.name == ":values").first();
					if (values == null) throw new Error("A Valued Enum must have a @:values meta which indicates the type of its fields", e.pos);
					valuesTypeStr = MacroTools.getString(values.params[0], true, true);
					valuesType = MacroTools.stringToComplexType(valuesTypeStr);

					if (e.names.length == 0)
					{
						throw new Error("Empty enum: Are you sure the class was declared after the enum?", e.pos);
					}

					for (name in e.names)
					{
						var ctor = e.constructs.get(name);
						var value = ctor.meta.get().filter(function(m) return m.name == ":value").first();
						if (value == null) throw new Error("A @:value meta must be present on all enum fields in order for it to be a Valued Enum", ctor.pos);

						exprArray.push(value.params[0]);
					}
				default: throw new Error("Argument must be an Enum Type.", subjectMeta.pos);
			}

			var vtype = TypeTools.getType(valuesType, pos);

			function isBit(val:Type):Bool
			{
				return switch(val)
				{
					case TType(t, _):
						if (t.toString() == "comtacti.native.options.Bit")
							true;
						else
							isBit(Context.follow(val, true));
					default: false;
				}
			}
			
			if (subjectMetas.length == 1)
			{
				if (isBit(vtype))
				{
					var enumsType = TPath({pack:[], name:"Array", params:[TPType(enumComplexType)], sub:null});
					fields.push(
						{
							name:"and",
							meta:[],
							kind:FFun({
								ret:enumsType,
								params:[],
								args:[
									{value:null, type:enumsType, opt:false, name:"enums"},
									{value:null, type:enumComplexType, opt:false, name:"_enum"}
								],
								expr:{
									expr:EBlock(
										[
											MacroTools.mkCall(["enums", "push"], [{expr:EConst(CIdent("_enum")), pos:pos}], pos),
											{expr:EReturn({expr:EConst(CIdent("enums")), pos:pos}), pos:pos}
										]),
									pos:pos
								}
							}),
							pos:pos,
							doc:null,
							access:[APublic, AStatic]
					});
					fields.push(
						{
							name:"toValue",
							meta:[],
							kind:FFun({
								ret:valuesType,
								params:[],
								args:[{value:null, type:TPath({pack:[], name:"Array", params:[TPType(enumComplexType)], sub:null}), opt:false, name:"enums"}],
								expr:
								{
									expr:EBlock([
										{expr: EVars([{type:null, name:"ret", expr:{expr:EConst(CInt("0")), pos:pos}}]), pos:pos},
										{expr: EFor({expr:EIn({expr:EConst(CIdent("_enum")), pos:pos}, {expr:EConst(CIdent("enums")), pos:pos}), pos:pos}, 
											{expr:EBinop(OpAssignOp(OpOr),
												{expr:EConst(CIdent("ret")), pos:pos}, 
												{expr:EArray({expr:EConst(CIdent(clsName + "Values")), pos:pos}, MacroTools.mkCall(["Type", "enumIndex"], [{expr:EConst(CIdent("_enum")), pos:pos}], pos)), pos:pos}
												),
											pos:pos}), pos:pos},
										{expr:EReturn({expr:EConst(CIdent("ret")), pos:pos}), pos:pos}
									]),
									pos:pos
								}
							}),
							pos:pos,
							doc:null,
							access:[APublic, AStatic, AInline]
					});
				} else {
					fields.push(
						{
							name:"toValue",
							meta:[],
							kind:FFun({
								ret:valuesType,
								params:[],
								args:[{value:null, type:enumComplexType, opt:false, name:"_enum"}],
								expr:{
									expr:EReturn({
										expr: EArray({expr:EConst(CIdent(clsName + "Values")), pos:pos}, MacroTools.mkCall(["Type", "enumIndex"], [{expr:EConst(CIdent("_enum")), pos:pos}], pos)),
										pos:pos
									}),
									pos:pos
								}
							}),
							pos:pos,
							doc:null,
							access:[APublic, AStatic, AInline]
						}
					);
				}
				
				fields.push({
					name:clsName + "Values",
					meta:[],
					kind:FieldType.FVar(TPath({pack:[], name:"Array", params:[TPType(valuesType)], sub:null}),
						{
							expr:EArrayDecl(exprArray),
							pos:pos
						}),
					pos:pos,
					doc:null,
					access:[APublic, AStatic]
				});
				
				var path = cls.pack.copy();
				path.push(cls.name);
				path.push(clsName + "Values");
				valuedEnumResolvers.set(subjectPath, {resolverPath:path, valueType:valuesType});
			} else {
				//throw new Error("Cannot have more than one subject.", pos);
				//TODO allow more than one subject. In order to do this, we have to find a solution for
				//the order in which haxe finds the enum class and then finds the helper class
				var clsName = subjectPathName;
				fields.push({
					name:clsName + "Values",
					meta:[],
					kind:FieldType.FVar(TPath({pack:[], name:"Array", params:[TPType(valuesType)], sub:null}),
						{
							expr:EArrayDecl(exprArray),
							pos:pos
						}),
					pos:pos,
					doc:null,
					access:[APublic, AStatic]
				});
				
				var path = cls.pack.copy();
				path.push(cls.name);
				path.push(clsName + "Values");
				valuedEnumResolvers.set(subjectPath, {resolverPath:path, valueType:valuesType});
			}
			
		}
		
		return fields;
	}
}